﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

namespace HossMvp.Core
{
    public abstract class AbstractContentProvider : IContentProvider
    {
        private readonly Dictionary<string, Func<ActionContext, ActionResult>> actions;
        private readonly Hashtable data;

        /// <summary>
        /// Initializes a new instance of the <see cref="AbstractContentProvider"/> class.
        /// </summary>
        /// <param name="decorator"></param>
        protected AbstractContentProvider(NameDecorator decorator)
        {
            this.Decorator = decorator;
            this.actions = new Dictionary<string, Func<ActionContext, ActionResult>>();
            this.data = new Hashtable();
        }

        public NameDecorator Decorator { get; private set; }

        /// <summary>
        /// Sets the value to the property with the specified name.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name">The name of the property.</param>
        /// <param name="value">The value.</param>
        public virtual void Set<T>(string name, T value)
        {
            var fullName = this.Augment(name);
            this.data[fullName] = value;
        }

        /// <summary>
        /// Gets the property with the specified name.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name">The name of the property.</param>
        /// <returns>The property</returns>
        /// <remarks>
        /// This is an alternative to packing properties as well as other data 
        /// to return values, or even to the passed ActionContext.
        /// </remarks>
        public virtual T Get<T>(string name)
        {
            var concretteName = this.Disambiguate(name);
            var value = this.data[concretteName];
            T result;
            try
            {
                result = (T)value;
            }
            catch
            {
                try
                {
                    result = (T)Convert.ChangeType(value, typeof(T));
                }
                catch
                {
                    result = default(T);
                }
            }

            return result;
        }

        /// <summary>
        /// Registers the action.
        /// </summary>
        /// <param name="actionName">Name of the action.</param>
        /// <param name="body">The body.</param>
        public virtual void RegisterAction(string actionName, Func<ActionContext, ActionResult> body)
        {
            this.actions[actionName] = body;
        }

        /// <summary>
        /// Registers all the actions using reflection. Internally calls RegisterAction().
        /// </summary>
        /// <remarks>
        /// This is a "convenience" method. It does not change content's provider basic functionality.
        /// </remarks>
        public virtual void RegisterActions()
        {
            var type = this.GetType();
            var candidates = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(a => a.ReturnType == typeof(ActionResult));

            var methodInfos = candidates.Select(a => new { a, parameters = a.GetParameters() })
                .Where(arg => arg.parameters.Count() == 1)
                .Select(p => new { p, pi = p.parameters[0] })
                .Where(t => t.pi.ParameterType == typeof(ActionContext))
                .Select(r => r.p.a);

            foreach (var info in methodInfos)
            {
                var func = Delegate.CreateDelegate(typeof(Func<ActionContext, ActionResult>), this, info);
                this.RegisterAction(info.Name, (Func<ActionContext, ActionResult>)func);
            }
        }

        public virtual void RegisterControls<T>(object container)
        {
            var type = container.GetType();

            // fields
            var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic).
                Where(ctrl => typeof(T).IsAssignableFrom(ctrl.FieldType));
            foreach (var fieldInfo in fieldInfos)
            {
                this.Set(fieldInfo.Name, fieldInfo.GetValue(container));
            }

            // properties
            var propInfos = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic)
                .Where(ctrl => typeof(T).IsAssignableFrom(ctrl.PropertyType));
            foreach (var propInfo in propInfos)
            {
                this.Set(propInfo.Name, propInfo.GetValue(container));
            }
        }

        /// <summary>
        /// Unregisters the action.
        /// </summary>
        /// <param name="actionName">Name of the action.</param>
        public virtual void UnregisterAction(string actionName)
        {
            this.actions.Remove(actionName);
        }

        /// <summary>
        /// Executes the action.
        /// </summary>
        /// <param name="actionName">Name of the action.</param>
        /// <param name="context">The context.</param>
        /// <returns></returns>
        public virtual ActionResult ExecuteAction(string actionName, ActionContext context)
        {
            var action = this.actions[actionName];
            var result = action.Invoke(context);
            return result;
        }

        protected virtual string Augment(string name)
        {
            if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("name is null or empty"); }
            if (name.StartsWith(".") || name.EndsWith(".")) { throw new ArgumentException("name should not start or end with '.'"); }

            var result = name; 
            if (name.Contains(".") == false)
            {
                // ReSharper disable once PossibleNullReferenceException
                var typeName = new StackFrame(2).GetMethod().DeclaringType.Name;
                result = typeName + "." + name;
            }

            return result;
        }

        protected virtual string Disambiguate(string name)
        {
            var names = this.data.Keys.Cast<string>().Where(n => n.EndsWith(name)).ToArray();
            switch (names.Length)
            {
                case 0:
                    return null;
                case 1:
                    return names[0];
                default:
                    var nameList = string.Join(", ", names);
                    throw new ArgumentException("Name ambiguity: {0}", nameList);
            }
        }
    }
}
